package cn.facetoken.homer;

import android.app.Activity;
import android.content.ContentValues;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.nnf.nnfkitlib.NNF_FaceAttribute;
import com.nnf.nnfkitlib.NNF_FaceInfo;
import com.nnf.nnfkitlib.NNF_FacePose;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

import javax.annotation.Nonnull;

import cn.openbiotoken.BiometricsDetector;
import cn.openbiotoken.BiometricsFactory;
import cn.openbiotoken.Landmark;
import cn.openbiotoken.android.UriBitmapHelper;
import cn.openbiotoken.android.facetoken.HomoSapiensFaceUriBiometricsFactory;
import cn.openbiotoken.impl.LandmarkImpl;

class PhotoTaker {

    public PhotoTaker(final @NonNull PhotoTakingPrincipal photoTakingPrincipal, final @NonNull Activity activity, final @NonNull ImageView imageView) {
        super();
        this.photoTakingPrincipal = photoTakingPrincipal;
        this.activity = activity;
        this.imageView = imageView;
        this.timestampPattern = activity.getResources().getString(R.string.timestamp_pattern);
    }

    @NonNull
    private final Activity activity;
    @NonNull
    private final ImageView imageView;
    @NonNull
    private final PhotoTakingPrincipal photoTakingPrincipal;

    private transient String currentPhotoPath;

    private transient Uri currentPhotoUri;

    private final String timestampPattern;

    public void takePhoto() {
        Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (null == takePhotoIntent.resolveActivity(activity.getPackageManager())) {
            return;
        }

        try {
            createPhotoFile();
        } catch (IOException e) {
            Log.e(getClass().getSimpleName(), "takePhoto(): Failed to create photo file.");
            throw new RuntimeException(e);
        }

        MediaStore.Images.Media.getContentUri("1");
        ContentValues contentValues = new ContentValues(1);
        currentPhotoUri = activity.getApplication().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
        Log.i(getClass().getSimpleName(), String.format("takePhoto(): External content URI is %s.", currentPhotoUri));
        takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, currentPhotoUri);
        takePhotoIntent.putExtra(MediaStore.EXTRA_SHOW_ACTION_ICONS, Boolean.TRUE);

        photoTakingPrincipal.startTakePhoto(takePhotoIntent);
    }

    public Uri onPhotoTaken(final int resultCode, @Nullable final Intent data) {
        Uri result = null;
        switch (resultCode) {
            case Activity.RESULT_OK:
                Log.d(getClass().getSimpleName(), "onPhotoTaken(): Photo was taken successfully.");
                if (null != data) {
                    final String output = Objects.requireNonNull(data.getExtras()).getString(MediaStore.EXTRA_OUTPUT);
                    Log.d(getClass().getSimpleName(), String.format("onPhotoTaken(): Photo URI from Intent = %s.", output));
                }
                result = currentPhotoUri;
                onPhotoTakenSuccess(data);
                break;
            case Activity.RESULT_CANCELED:
                Log.d(getClass().getSimpleName(), "onPhotoTaken(): Taking photo was cancelled.");
                onPhotoTakenCanceled(data);
                break;
            case Activity.RESULT_FIRST_USER:
                Log.d(getClass().getSimpleName(), "onPhotoTaken(): Taking photo result is user defined.");
                break;
            default:
                Log.d(getClass().getSimpleName(), "onPhotoTaken(): Taking photo result is unknown.");
        }

        currentPhotoPath = null;
        currentPhotoUri = null;

        return result;
    }

    private void onPhotoTakenSuccess(@Nullable final Intent data) {
        if (null == data) {
            Log.d(getClass().getSimpleName(), "onPhotoTakenSuccess(): Data of photo taken is null. Call showPhotoFile().");
            showPhotoFile();
            return;
        }
        if (null == data.getExtras()) {
            Log.d(getClass().getSimpleName(), "onPhotoTakenSuccess(): Data extra of photo taken is null. Call showPhotoFile().");
            showPhotoFile();
            return;
        }

        showPhotoFile(data);
    }

    private void onPhotoTakenCanceled(Intent data) {

        deletePhotoFile();

        if (null == data) {
            Log.d(getClass().getSimpleName(), "onPhotoTakenCanceled(): Data of canceled photo-taking is null. Ignore the rest operations.");
        }
    }

    private File createPhotoFile() throws IOException {

        final String storageState = Environment.getExternalStorageState();
        File storageDirectory;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            Log.i(getClass().getSimpleName(), "createPhotoFile(): External storage is mounted.");
            //storageDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
            storageDirectory = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        } else {
            Log.w(getClass().getSimpleName(), String.format("createPhotoFile(): External storage is not mounted (state = %s). Use internal storage instead.", storageState));
            //storageDirectory = Environment.getDataDirectory();
            storageDirectory = activity.getFilesDir();
        }

        if (null == storageDirectory) {
            Log.e(getClass().getSimpleName(), "createPhotoFile(): Storage directory is null. Ignore the rest operations.");
            throw new IOException("Storage directory should not be null.");
        }

        Log.i(getClass().getSimpleName(), String.format("createPhotoFile(): Storage directory is %s. Exists? %s. Writable? %s.", storageDirectory.getAbsolutePath(), storageDirectory.exists(), storageDirectory.canWrite()));
        if (storageDirectory.canWrite()) {
            Log.i(getClass().getSimpleName(), String.format("Can write into %s.", storageDirectory.getAbsolutePath()));
        } else {
            Log.e(getClass().getSimpleName(), String.format("Can NOT write into %s.", storageDirectory.getAbsolutePath()));
        }

        File photoFile = File.createTempFile(
                String.format("FaceToken_Homer_%s", new SimpleDateFormat(timestampPattern).format(new Date())),  /* prefix */
                ".jpg",         /* suffix */
                storageDirectory      /* directory */
        );

        // Save a file: path for use with ACTION_VIEW intents
        currentPhotoPath = photoFile.getAbsolutePath();
        Log.w(getClass().getSimpleName(), String.format("createPhotoFile(): Photo will be saved into %s. Writable? %s.", currentPhotoPath, photoFile.canWrite()));
        return photoFile;
    }

    private void deletePhotoFile() {

        if (null == currentPhotoPath) {
            return;
        }

        File file = new File(currentPhotoPath);
        if (file.exists()) {
            if (file.delete()) {
                Log.d(getClass().getSimpleName(), String.format("deletePhotoFile(): File %s was deleted.", currentPhotoPath));
            } else {
                Log.w(getClass().getSimpleName(), String.format("deletePhotoFile(): Failed to delete file %s..", currentPhotoPath));
            }
        }
    }

    private void showPhotoFile() {
        Log.d(getClass().getSimpleName(), String.format("showPhotoFile(): Current Photo URI = %s.", currentPhotoUri));

        imageView.setTag(R.id.tag_key_photo_uri, currentPhotoUri);
        imageView.setTag(R.id.tag_key_photo_path, currentPhotoPath);

        @NonNull final BiometricsFactory<NNF_FaceInfo, float[], Uri> factory = new HomoSapiensFaceUriBiometricsFactory(activity);
        @NonNull final BiometricsDetector<NNF_FaceInfo> detector = factory.createDetector(currentPhotoUri);
        @Nonnull final NNF_FaceInfo face = Objects.requireNonNull(detector.detect());

        setupFacePhoto(face);
        setupFace(face);
        setupFacePose(face);
        setupFaceAttribute(face);
        setupDynamicActionArea(face);
    }

    private void setupFacePhoto(@Nonnull final NNF_FaceInfo face) {
        final int left = face.faceRect.left;
        final int top = face.faceRect.top;
        final int width = face.faceRect.width;
        final int height = face.faceRect.height;

        @Nonnull final Rect rect = new Rect(left, top, left + width, top + height);

        @Nonnull final UriBitmapHelper bitmapHelper = new UriBitmapHelper(activity, currentPhotoUri);
        @Nonnull final Bitmap bitmap = bitmapHelper.loadBitmap().copy(Bitmap.Config.ARGB_8888, true);
        bitmapHelper.drawRect(bitmap, rect);
        imageView.setImageDrawable(new BitmapDrawable(activity.getResources(), bitmap));

        @Nullable final TextView maxFaceText = activity.findViewById(R.id.field_max_face);
        if (null != maxFaceText) {
            maxFaceText.setText(activity.getResources().getString(R.string.field_face_detected, left, top, width, height));
        }
    }

    private void setupFace(@Nonnull final NNF_FaceInfo face) {
        showText(R.id.field_face_count, face.faceNum, false);
        showText(R.id.field_face_track_id, face.faceTrackID, false);
        showText(R.id.field_face_light, face.facelight, true);
        showText(R.id.field_face_clarity, face.faceClarity, true);
        showText(R.id.field_face_completeness, face.faceCompleteness, true);
        showScore(R.id.field_face_score, face.faceScore);
        showText(R.id.field_face_big_angle, face.bigAngle);
    }

    private void setupFacePose(@Nonnull final NNF_FaceInfo face) {
        @Nullable final NNF_FacePose facePose = face.facePose;
        if (null != facePose) {
            showText(R.id.field_face_yaw, facePose.yaw, false);
            showText(R.id.field_face_pitch, facePose.pitch, false);
            showText(R.id.field_face_roll, facePose.roll, false);
        }
    }

    private void setupFaceAttribute(@Nonnull final NNF_FaceInfo face) {
        @Nullable final NNF_FaceAttribute faceAttribute = face.faceAttribute;
        if (null != faceAttribute) {
            showText(R.id.field_face_gender, getGenderText(faceAttribute));
            showScore(R.id.field_face_gender_score, faceAttribute.genderScore);
            showText(R.id.field_face_age, faceAttribute.age, false);
            showScore(R.id.field_face_age_score, faceAttribute.ageScore);
            showText(R.id.field_face_mask, getMaskText(faceAttribute));
            showScore(R.id.field_face_mask_score, faceAttribute.maskScore);
        }
    }

    private void setupDynamicActionArea(@Nonnull final NNF_FaceInfo face) {
        final ViewGroup dynamicActionArea = activity.findViewById(R.id.dynamic_action_area);
        if (null != dynamicActionArea) {
            for (int i = 0; i < dynamicActionArea.getChildCount(); i++) {
                @Nonnull final View view = dynamicActionArea.getChildAt(i);
                final Object tagName = view.getTag(R.id.tag_key_name);
                if ("landmarks5".equals(tagName)) {
                    view.setTag(R.id.tag_key_data, String.format("landmarks5 data: %s, score: %s", Arrays.toString(face.landmarks5), Arrays.toString(face.landmarksScore5)));
                    @Nonnull final ImageView imageView = (ImageView)dynamicActionArea.getChildAt(i-1);
                    setupImageWithLandmarks(imageView, convertLandmarks(face.landmarks5, face.landmarksScore5));
                } else if ("landmarks106".equals(tagName)) {
                    view.setTag(R.id.tag_key_data, String.format("landmarks106 data: %s, score: %s", Arrays.toString(face.landmarks106), Arrays.toString(face.landmarksScore106)));
                    @Nonnull final ImageView imageView = (ImageView)dynamicActionArea.getChildAt(i-1);
                    setupImageWithLandmarks(imageView, convertLandmarks(face.landmarks106, face.landmarksScore106));
                }
            }
        }
    }

    private List<Landmark> convertLandmarks(@Nonnull final float[] landmarks, @Nonnull final float[] scores) {
        if (landmarks.length != scores.length * 2) {
            throw new IllegalArgumentException(String.format("convertLandmarks(): Wrong array length: Length of landmarks: %d, Length of scores: %d", landmarks.length, scores.length));
        }

        @Nonnull final ArrayList<Landmark> landmarkList = new ArrayList<>(scores.length);
        for (int i = 0; i < scores.length; i++) {
            landmarkList.add(new LandmarkImpl(landmarks[i * 2], landmarks[i * 2 + 1], (byte) scores[i]));
        }

        return landmarkList;
    }

    private void setupImageWithLandmarks(@Nonnull final ImageView imageView, @Nonnull final List<Landmark> landmarkList) {
        imageView.setVisibility(View.VISIBLE);

        @NonNull final UriBitmapHelper bitmapHelper = new UriBitmapHelper(activity, currentPhotoUri);
        @NonNull final Bitmap bitmap = bitmapHelper.loadBitmap().copy(Bitmap.Config.ARGB_8888, true);
        bitmapHelper.drawLandmarks(bitmap, landmarkList);
        imageView.setImageDrawable(new BitmapDrawable(activity.getResources(), bitmap));
    }

    @NonNull
    private String getMaskText(@NonNull final NNF_FaceAttribute faceAttribute) {
        int resourceId;
        switch (faceAttribute.mask) {
            case 0:
                resourceId = R.string.mask_disabled;
                break;
            case 1:
                resourceId = R.string.mask_no;
                break;
            case 2:
                resourceId = R.string.mask_yes;
                break;
            default:
                resourceId = R.string.field_face_mask_not_available;
        }
        return activity.getResources().getString(resourceId);
    }

    @NonNull
    private String getGenderText(@NonNull final NNF_FaceAttribute faceAttribute) {
        int resourceId;
        switch (faceAttribute.gender) {
            case 0:
                resourceId = R.string.gender_disabled;
                break;
            case 1:
                resourceId = R.string.gender_male;
                break;
            case 2:
                resourceId = R.string.gender_female;
                break;
            default:
                resourceId = R.string.field_face_gender_not_available;
        }
        return activity.getResources().getString(resourceId);
    }

    private void showText(final int resourceId, final int value, final boolean percentage) {
        @Nullable final TextView text = activity.findViewById(resourceId);
        if (null != text) {
            final StringBuilder buffer = new StringBuilder();
            buffer.append(value);
            if (percentage) {
                buffer.append("%");
            }
            text.setText(buffer.toString());
        }
    }

    private void showScore(final int resourceId, final float percentage) {
        @Nullable final TextView text = activity.findViewById(resourceId);
        if (null != text) {
            text.setText(String.format(Locale.getDefault(), "%5.2f%%", percentage * 100));
        }
    }

    private void showText(final int resourceId, final boolean value) {
        @Nullable final TextView text = activity.findViewById(resourceId);
        if (null != text) {
            text.setText(value ? R.string.yes : R.string.no);
        }
    }

    private void showText(final int resourceId, final String value) {
        @Nullable final TextView text = activity.findViewById(resourceId);
        if (null != text) {
            text.setText(value);
        }
    }

    private void showPhotoFile(Intent data) {

        Bundle extras = data.getExtras();
        if (null == extras) {
            Log.w(getClass().getSimpleName(), "showPhotoFile(): intent.extras is null. Ignore the rest operations.");
            return;
        }

        Bitmap bitmap = (Bitmap) extras.get("data");

        if (null == bitmap) {
            Log.w(getClass().getSimpleName(), "showPhotoFile(): intent.extras.data is null. Ignore the rest operations.");
            return;
        }

        for (String key : extras.keySet()) {
            Log.d(getClass().getSimpleName(), "showPhotoFile(): intent.extras.keys");
            Log.d(getClass().getSimpleName(), String.format("showPhotoFile():   %s", key));
        }
        Log.d(getClass().getSimpleName(),
                String.format("showPhotoFile(): Bitmap taken: byteCount = %d, rowBytes = %d, density = %d, height = %d, width = %d, config = %s",
                        bitmap.getByteCount(), bitmap.getRowBytes(), bitmap.getDensity(),
                        bitmap.getHeight(), bitmap.getWidth(), bitmap.getConfig().name()));
        imageView.setImageBitmap(bitmap);
    }
}